From 2e02765e4b79d0d145520f9005c75d382805dc2e Mon Sep 17 00:00:00 2001 From: diogo464 Date: Mon, 11 Aug 2025 16:28:59 +0100 Subject: implement RESTful API and remove legacy endpoints MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Created unified /api/fs/[...path] endpoint with full REST methods: - GET: List directory contents or file info - POST: Create directories using Drive_mkdir() - PUT: Upload files with multipart form data - DELETE: Remove files/directories using Drive_remove() - Added /api/fs route for root directory listing - Added Drive_mkdir() function to drive_server.ts using fctdrive mkdir command - Removed legacy /api/delete and /api/upload endpoints - Updated CLAUDE.md with comprehensive API documentation and examples - All endpoints support authentication with AUTH: 1 header in development - Proper error handling, file size validation, and cache revalidation 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- frontend/app/api/fs/[...path]/route.ts | 209 +++++++++++++++++++++++++++++++++ 1 file changed, 209 insertions(+) create mode 100644 frontend/app/api/fs/[...path]/route.ts (limited to 'frontend/app/api/fs/[...path]') diff --git a/frontend/app/api/fs/[...path]/route.ts b/frontend/app/api/fs/[...path]/route.ts new file mode 100644 index 0000000..3a299af --- /dev/null +++ b/frontend/app/api/fs/[...path]/route.ts @@ -0,0 +1,209 @@ +import { NextRequest, NextResponse } from 'next/server' +import { writeFile, unlink } from 'fs/promises' +import { tmpdir } from 'os' +import { join } from 'path' +import { randomUUID } from 'crypto' +import { Auth_get_user, Auth_user_can_upload } from '@/lib/auth' +import { Drive_ls, Drive_remove, Drive_mkdir, Drive_import } from '@/lib/drive_server' +import { UPLOAD_MAX_FILE_SIZE } from '@/lib/constants' +import { revalidatePath } from 'next/cache' + +// GET /api/fs/path/to/file - Get file/directory listing +export async function GET( + request: NextRequest, + { params }: { params: Promise<{ path: string[] }> } +) { + try { + const { path: pathSegments } = await params + const filePath = '/' + (pathSegments?.join('/') || '') + + // Get directory listing using Drive_ls (non-recursive) + const entries = await Drive_ls(filePath, false) + + return NextResponse.json(entries) + + } catch (error) { + console.error('GET fs error:', error) + return NextResponse.json( + { error: error instanceof Error ? error.message : 'Internal server error' }, + { status: 500 } + ) + } +} + +// DELETE /api/fs/path/to/file - Delete file/directory +export async function DELETE( + request: NextRequest, + { params }: { params: Promise<{ path: string[] }> } +) { + try { + // Check user authentication and permissions + const user = await Auth_get_user() + if (!user.isLoggedIn) { + return NextResponse.json({ error: 'User not authenticated' }, { status: 401 }) + } + + if (!Auth_user_can_upload(user)) { + return NextResponse.json({ error: 'User does not have upload permissions' }, { status: 403 }) + } + + const { path: pathSegments } = await params + const filePath = '/' + (pathSegments?.join('/') || '') + + // Remove file/directory using Drive_remove + await Drive_remove(filePath, user.email) + + // Revalidate the parent directory to refresh listings + const parentPath = filePath.split('/').slice(0, -1).join('/') || '/' + revalidatePath(`/drive${parentPath}`) + revalidatePath('/drive') + + return NextResponse.json({ + success: true, + message: 'Path deleted successfully', + deletedPath: filePath + }) + + } catch (error) { + console.error('DELETE fs error:', error) + return NextResponse.json( + { error: error instanceof Error ? error.message : 'Internal server error' }, + { status: 500 } + ) + } +} + +// PUT /api/fs/path/to/file - Create/upload file +export async function PUT( + request: NextRequest, + { params }: { params: Promise<{ path: string[] }> } +) { + try { + // Check user authentication and permissions + const user = await Auth_get_user() + if (!user.isLoggedIn) { + return NextResponse.json({ error: 'User not authenticated' }, { status: 401 }) + } + + if (!Auth_user_can_upload(user)) { + return NextResponse.json({ error: 'User does not have upload permissions' }, { status: 403 }) + } + + const { path: pathSegments } = await params + const filePath = '/' + (pathSegments?.join('/') || '') + + // Check if request has file content + const contentType = request.headers.get('content-type') + if (!contentType || (!contentType.includes('multipart/form-data') && !contentType.includes('application/octet-stream'))) { + return NextResponse.json({ + error: 'Content-Type must be multipart/form-data or application/octet-stream' + }, { status: 400 }) + } + + let fileBuffer: Buffer + let filename: string + + if (contentType.includes('multipart/form-data')) { + // Handle multipart form data + const formData = await request.formData() + const file = formData.get('file') as File + + if (!file) { + return NextResponse.json({ error: 'No file provided' }, { status: 400 }) + } + + if (file.size > UPLOAD_MAX_FILE_SIZE) { + return NextResponse.json({ + error: `File exceeds maximum size of ${UPLOAD_MAX_FILE_SIZE / (1024 * 1024)}MB` + }, { status: 400 }) + } + + const bytes = await file.arrayBuffer() + fileBuffer = Buffer.from(bytes) + filename = file.name + } else { + // Handle raw binary data + const bytes = await request.arrayBuffer() + fileBuffer = Buffer.from(bytes) + + if (fileBuffer.length > UPLOAD_MAX_FILE_SIZE) { + return NextResponse.json({ + error: `File exceeds maximum size of ${UPLOAD_MAX_FILE_SIZE / (1024 * 1024)}MB` + }, { status: 400 }) + } + + // Extract filename from path + filename = pathSegments?.[pathSegments.length - 1] || 'upload' + } + + // Create temporary file + const tempFileName = `${randomUUID()}-${filename}` + const tempFilePath = join(tmpdir(), tempFileName) + + // Save file to temporary location + await writeFile(tempFilePath, fileBuffer) + + // Import file using Drive_import (uses --mode move, so temp file is already deleted) + await Drive_import(tempFilePath, filePath, user.email) + + // Revalidate the parent directory to refresh listings + const parentPath = filePath.split('/').slice(0, -1).join('/') || '/' + revalidatePath(`/drive${parentPath}`) + revalidatePath('/drive') + + return NextResponse.json({ + success: true, + message: 'File uploaded successfully', + path: filePath + }) + + } catch (error) { + console.error('PUT fs error:', error) + return NextResponse.json( + { error: error instanceof Error ? error.message : 'Internal server error' }, + { status: 500 } + ) + } +} + +// POST /api/fs/path/to/directory - Create directory +export async function POST( + request: NextRequest, + { params }: { params: Promise<{ path: string[] }> } +) { + try { + // Check user authentication and permissions + const user = await Auth_get_user() + if (!user.isLoggedIn) { + return NextResponse.json({ error: 'User not authenticated' }, { status: 401 }) + } + + if (!Auth_user_can_upload(user)) { + return NextResponse.json({ error: 'User does not have upload permissions' }, { status: 403 }) + } + + const { path: pathSegments } = await params + const dirPath = '/' + (pathSegments?.join('/') || '') + + // Create directory using Drive_mkdir + await Drive_mkdir(dirPath, user.email) + + // Revalidate the parent directory to refresh listings + const parentPath = dirPath.split('/').slice(0, -1).join('/') || '/' + revalidatePath(`/drive${parentPath}`) + revalidatePath('/drive') + + return NextResponse.json({ + success: true, + message: 'Directory created successfully', + path: dirPath + }) + + } catch (error) { + console.error('POST fs error:', error) + return NextResponse.json( + { error: error instanceof Error ? error.message : 'Internal server error' }, + { status: 500 } + ) + } +} \ No newline at end of file -- cgit